//
//  XFDownloadManager.swift
//  XFDownloadDemo
//
//  Created by mac on 2018/11/5.
//  Copyright © 2018 zeroLu. All rights reserved.
//

import UIKit
import Foundation

private let filesHadWrittenDataKey = "filesHadWrittenDataKey"

private let filesTotalLengthDataKey = "XFfilesTotalLengthDataKey"

private let emptyURL = URL(string: "https://")!

class XFDownloadManager: NSObject {
    
    public static let shared = XFDownloadManager.init()
    
    public var maxConcurrentCount: Int = -1
    
    public var downloadDirectory: String = "" {
        didSet {
            if !downloadDirectory.isEmpty {
                createDirectory(downloadDirectory)
            }
        }
    }
    
    public var backgroundCompletionHandler: (() -> Void)?
    
    private let _fileManager = FileManager.default
    
    private let _downloadQueue = OperationQueue.init()
    
    private lazy var _session: URLSession = {
        
        let session = URLSession(configuration: URLSessionConfiguration.default,
                                 delegate: self,
                                 delegateQueue: _downloadQueue)
        return session
    }()
    
    private(set) var _downloadModels = [String: XFDownloadModel]()
    
    private(set) var _downloadingModels = [XFDownloadModel]()
    
    private(set) var _waitingModels = [XFDownloadModel]()
    
    private(set) var fileHadWrittenData: [String: Int64] = {
        
        var filesData = [String: Int64]()
        
        if let localData = UserDefaults.standard.value(forKey: filesHadWrittenDataKey) as? [String: Int64] {
            
            filesData = localData
        } else {
            UserDefaults.standard.set(filesData, forKey: filesHadWrittenDataKey)
            UserDefaults.standard.synchronize()
        }
        
        return filesData
        
        }() {
        
        didSet {
            
            DispatchQueue.global().async {
                UserDefaults.standard.set(self.fileHadWrittenData, forKey: filesHadWrittenDataKey)
                UserDefaults.standard.synchronize()
            }
        }
    }
    
    private(set) var fileTotalExpectedData: [String: Int64] = {
        
        var filesData = [String: Int64]()
        
        if let localData = UserDefaults.standard.value(forKey: filesTotalLengthDataKey) as? [String: Int64] {
            
            filesData = localData
        } else {
            UserDefaults.standard.set(filesData, forKey: filesTotalLengthDataKey)
            UserDefaults.standard.synchronize()
        }
        
        return filesData
        
        }() {
        
        didSet {
            
            DispatchQueue.global().async {
                UserDefaults.standard.set(self.fileTotalExpectedData, forKey: filesTotalLengthDataKey)
                UserDefaults.standard.synchronize()
            }
        }
    }
    
    private override init() {
        super.init()
        
        if let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
            
            print("~~~~~~~~~~~~~~\(path)~~~~~~~~~~~~~~")
            
            downloadDirectory = NSString(string: path).appendingPathComponent("XFDownloadManager")
            
            createDirectory(downloadDirectory)
        }
    }
}

extension XFDownloadManager: URLSessionDataDelegate {
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        
        guard let _url = response.url,
            let downloadModel = _downloadModels[_url.fileName()] else {
                return
        }
        
        let receivedSize = hasDownloadedLength(url: _url)
        
        let totalExpectedLength = response.expectedContentLength + receivedSize
        
        if receivedSize == totalExpectedLength {
            
            _downloadingModels.removeAll(where: { $0.url == _url })
            
            _downloadModels.removeValue(forKey: _url.fileName())
            
            return
        }
        
        downloadModel.openOutputStream()
        
        downloadModel.totalLength = totalExpectedLength
        
        fileTotalExpectedData[_url.fileName()] = totalExpectedLength
        
        completionHandler(URLSession.ResponseDisposition.allow)
    }
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        
        guard let _url = dataTask.response?.url,
            let downloadModel = _downloadModels[_url.fileName()] else {
                return
        }
        
        let receivedSize = hasDownloadedLength(url: _url)
        
        let expectedSize = downloadModel.totalLength
        
        if downloadModel.outputStream?.streamStatus == Stream.Status.closed {
            downloadModel.outputStream?.open()
        }
        
        downloadModel.outputStream?.write(data.bytes, maxLength: data.count)
        
        if let hadWritten = fileHadWrittenData[_url.fileName()] {
            fileHadWrittenData[_url.fileName()] = hadWritten + Int64(data.count)
        }
        
        let progress = Float(receivedSize) / Float(expectedSize)
        
        downloadModel.progressBlock?(_url, receivedSize, expectedSize, progress)
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    
        guard let _url = task.currentRequest?.url,
            let downloadModel = _downloadModels[_url.fileName()] else {
                resumeNextDowloadModel()
                return
        }
        
        downloadModel.closeOutputStream()
        
        if error == nil {
            _downloadModels.removeValue(forKey: _url.fileName())
            
            if _downloadingModels.count > 0 {
                _downloadingModels.removeAll(where: { $0.url == _url })
            }
            
            if isDownloadCompleted(url: _url) {
                
                let fullPath = getFilePath(_url)
                
                let destPath = downloadModel.destPath
                
                if !destPath.isEmpty {
                    do {
                        try _fileManager.moveItem(atPath: fullPath, toPath: destPath)
                    } catch {
                        print(error)
                    }
                }
                
                downloadModel.stateBlock?(_url, XFDownloadState.completed)
                
                downloadModel.completionBlock?(_url, true, destPath.isEmpty ? fullPath : destPath, nil)
            }
            
            resumeNextDowloadModel()
        } else {
            guard let _error = error as NSError? else {
                downloadModel.stateBlock?(_url, XFDownloadState.failed)
                downloadModel.completionBlock?(_url, false, "", error)
                resumeNextDowloadModel()
                return
            }
            
            if _error.code == -999 {       // 下载任务取消的状态码
                return
            }
        }
    }
}

// MARK: - Download Methods
extension XFDownloadManager {
    
    /// 开始下载
    ///
    /// - Parameters:
    ///   - url: 下载链接
    ///   - destPath: 下载完成后存放的路径
    ///   - stateBlock: 状态回调
    ///   - progressBlock: 进度回调
    ///   - completionBlock: 下载完成回调
    func download(url: URL?, destPath: String = "", stateBlock: XFDownloadStateBlock?, progressBlock: XFDownloadProgressBlock?, completionBlock: XFDownloadCompletionBlock?) {
        
        guard let _url = url else {
            fatalError("URL 不能为空")
        }
        
        if isDownloadCompleted(url: _url) {
            
            stateBlock?(_url, XFDownloadState.completed)
            
            completionBlock?(_url, true, fileSandBoxPath(url: _url), nil)
            
            _downloadModels.removeValue(forKey: _url.fileName())
            
            return
        }
        
        if let downloadModel = _downloadModels[_url.fileName()] {
            
            if (downloadModel.dataTask?.state == URLSessionDownloadTask.State.suspended || downloadModel.dataTask?.state == URLSessionDownloadTask.State.canceling) {
                
                resumeDownload(url: url)
                
            }
            
            return
        }
        
        let taskDescription = _url.fileName()
        
        var request = URLRequest(url: _url)
        
        let hasDownloadLength = hasDownloadedLength(url: _url)
        if hasDownloadLength > 0 {
            request.setValue("bytes=\(hasDownloadLength)-", forHTTPHeaderField: "Range")
        }
        
        let dataTask = _session.dataTask(with: request)
        dataTask.taskDescription = taskDescription
        
        let downloadModel = initDownloadModel(dataTask, _url, destPath, stateBlock, progressBlock, completionBlock)
        downloadModel.outputStream = OutputStream(toFileAtPath: getFilePath(_url), append: true)
        
        _downloadModels[taskDescription] = downloadModel
        
        var downloadState = XFDownloadState.running
        
        if canResumeDownload() {
            
            if _downloadingModels.contains(where: { $0.url == _url }) == false {
                _downloadingModels.append(downloadModel)
            }
            
            dataTask.resume()
        } else {
            if _waitingModels.contains(where: { $0.url == _url }) == false {
                _waitingModels.append(downloadModel)
            }
            downloadState = XFDownloadState.waiting
        }
        
        downloadModel.stateBlock?(_url, downloadState)
    }
    
    /// 自动判断当前下载任务的状态,开始或暂停
    ///
    /// - Parameter url: 下载链接
    func autoBeginOrSuspend(_ url: URL?, destPath: String = "", stateBlock: XFDownloadStateBlock? = nil, progressBlock: XFDownloadProgressBlock? = nil, completionBlock: XFDownloadCompletionBlock? = nil) {
        
        if let downloadModel = _downloadModels.filter({ $0.value.url == url }).first?.value {
            
            if downloadModel.dataTask?.state == URLSessionDataTask.State.suspended {
                resumeDownload(url: url)
            } else if downloadModel.dataTask?.state == URLSessionDataTask.State.running {
                suspendDownloadResumeNex(url: url, flag: true)
            }
        } else {
            download(url: url, destPath: destPath, stateBlock: stateBlock, progressBlock: progressBlock, completionBlock: completionBlock)
        }
        
    }
    
    /// 暂停下载
    ///
    /// - Parameter url: 下载链接
    func suspendDownload(_ url: URL?) {
        
        suspendDownloadResumeNex(url: url, flag: true)
    }
    
    /// 暂停所有下载任务
    func suspendDownloadAll() {
        
        if _downloadingModels.count == 0 {
            return
        }
        
        let downloadModels = _downloadModels.values
        
        for downloadModel in downloadModels {
            
            suspendDownloadResumeNex(url: downloadModel.url, flag: false)
        }
    }
    
    /// 取消下载
    ///
    /// - Parameter url: 下载链接
    func cancelDownload(_ url: URL?) {
        cancelDownloadResumeNex(url: url, flag: true)
    }
    
    /// 取消所有下载
    func cancelDownloadAll() {
        if _downloadModels.count == 0 {
            return
        }
        
        let downloadModels = _downloadModels.values
        
        for downloadModel in downloadModels {
            cancelDownloadResumeNex(url: downloadModel.url, flag: false)
        }
    }
}

// MARK: - Public Methods
extension XFDownloadManager {
    
    func isDownloadCompleted(url: URL?) -> Bool {
        
        let totalExpected = totalLength(url: url)
        
        if totalExpected == 0 {
            return false
        }
        
        return hasDownloadedLength(url: url) == totalExpected
    }
    
    func fileSandBoxPath(url: URL?) -> String {
        
        guard let _url = url else {
            return ""
        }
        
        return getFilePath(_url)
        
    }
    
    func hasDownloadProgress(url: URL?) -> Float {
        
        if isDownloadCompleted(url: url) {
            return 1.0
        }
        
        if totalLength(url: url) == 0 {
            return 0.0
        }
        
        return Float(hasDownloadedLength(url: url)) / Float(totalLength(url: url))
    }
    
    /// 删除下载任务
    ///
    /// - Parameter url: 下载链接
    func deleteDownloadFile(url: URL?) {
        
        guard let _url = url else {
            return
        }
        
        cancelDownload(_url)
        
        fileHadWrittenData.removeValue(forKey: _url.fileName())
        
        fileTotalExpectedData.removeValue(forKey: _url.fileName())
        
        let filePath = (downloadDirectory as NSString).appendingPathComponent(_url.fileName())
        
        if _fileManager.isExecutableFile(atPath: filePath) {
            do {
                try _fileManager.removeItem(atPath: filePath)
            } catch {
                print(error)
            }
        }
    }
    
    /// 删除所有下载
    func deleteDownloadFilesAll() {
        cancelDownloadAll()
        
        removeFileAll()
    }
    
    /// 已经下载的文件大小
    ///
    /// - Parameter url: 下载链接
    /// - Returns: 临时文件大小
    func hasDownloadedLength(url: URL?) -> Int64 {
        let filePath = fileSandBoxPath(url: url)
        
        if !filePath.isEmpty {
            do {
                let dict = try _fileManager.attributesOfItem(atPath: filePath)
                
                guard let fileLength = dict[FileAttributeKey.size] as? NSNumber else {
                    return 0
                }
                
                return fileLength.int64Value
            } catch {
                print(error)
            }
        }
        
        return 0
    }
}

// MARK: Private Methods
private extension XFDownloadManager {
    
    /// 是否可以开始下载
    ///
    /// - Returns: 布尔值
    func canResumeDownload() -> Bool {
        
        return ((maxConcurrentCount == -1) || (_downloadingModels.count < maxConcurrentCount))
    }
    
    /// 继续下一个任务
    func resumeNextDowloadModel() {
        
        if maxConcurrentCount == -1 {       // 没有下载任务个数限制,直接返回
            return
        }
        
        if _waitingModels.count == 0 {       // 队列中已经木有等待的任务了
            return
        }
        
        if let downloadModel = _waitingModels.first {
            
            if _waitingModels.count > 0 {
                _waitingModels.removeAll(where: { $0.url == downloadModel.url })
            }
            
            var downloadState = XFDownloadState.running
            
            if canResumeDownload() {
                
                if _downloadingModels.contains(where: { $0.url == downloadModel.url }) == false {
                    _downloadingModels.append(downloadModel)
                }
                
                downloadModel.dataTask?.resume()
                
            } else {
                _waitingModels.append(downloadModel)
                
                downloadState = XFDownloadState.waiting
            }
            
            downloadModel.stateBlock?(downloadModel.url, downloadState)
        }
    }
    
    /// 获取目标文件的总大小
    ///
    /// - Parameter url: 下载链接
    /// - Returns: 返回文件下载完成后的大小
    func totalLength(url: URL?) -> Int64 {
        
        guard let _url = url else {
            return 0
        }
        
        if fileTotalExpectedData.count == 0 {
            return 0
        }
        
        if let totalLength = fileTotalExpectedData[_url.fileName()] {
            return totalLength
        } else {
            return 0
        }
    }
    
    /// 取消下载
    ///
    /// - Parameters:
    ///   - url: 下载链接
    ///   - flag: 是否需要开始下一个在等待的任务
    func cancelDownloadResumeNex(url: URL?, flag: Bool) {
        
        guard let _url = url,
            let downloadModel = _downloadModels[_url.fileName()] else {
                return
        }
        
        downloadModel.dataTask?.cancel()
        
        downloadModel.stateBlock?(_url, XFDownloadState.canceled)
        
        if _waitingModels.count > 0 {
            _waitingModels.removeAll(where: { $0.url == _url })
        }
        
        _downloadModels.removeValue(forKey: _url.fileName())
        
        if _downloadingModels.count > 0 {
            _downloadingModels.removeAll(where: { $0.url == _url })
        }
        
        if flag {
            resumeNextDowloadModel()
        }
    }
    
    /// 暂停下载
    ///
    /// - Parameters:
    ///   - url: 下载链接
    ///   - flag: 是否需要开始下一个等待的任务
    func suspendDownloadResumeNex(url: URL?, flag: Bool) {
        
        guard let _url = url,
            let downloadModel = _downloadModels[_url.fileName()] else {
                return
        }
        
        downloadModel.dataTask?.suspend()
        
        downloadModel.stateBlock?(_url, XFDownloadState.suspended)
        
        if _downloadingModels.count > 0 {
            _downloadingModels.removeAll(where: { $0.url == _url })
        }
        
        if flag {
            resumeNextDowloadModel()
        }
    }
    
    /// 继续下载任务
    ///
    /// - Parameter url: 下载链接
    func resumeDownload(url: URL?) {
        guard let _url = url,
            let downloadModel = _downloadModels[_url.fileName()] else {
                return
        }
        
        var downloadState = XFDownloadState.running
        
        if canResumeDownload() {
            if _downloadingModels.contains(where: { $0.url == _url }) == false {
                _downloadingModels.append(downloadModel)
            }
            downloadModel.dataTask?.resume()
        } else {
            if _waitingModels.contains(where: { $0.url == _url }) == false {
                _waitingModels.append(downloadModel)
            }
            downloadState = XFDownloadState.waiting
        }
        
        downloadModel.stateBlock?(_url, downloadState)
    }
    
    /// 继续下载所有的任务
    func resumeDownloadAll() {
        
        if _downloadModels.count == 0 {
            
            return
        }
        
        let downloadModels = _downloadModels.values.sorted(by: { ($0.dataTask?.taskIdentifier ?? 0) < ($1.dataTask?.taskIdentifier ?? 0) })
        
        for downloadModel in downloadModels {
            resumeDownload(url: downloadModel.url)
        }
    }
    
    /// 获取文件绝对路径
    ///
    /// - Parameter url: 文件下载链接
    /// - Returns: 绝对路径
    func getFilePath(_ url: URL) -> String {
        
        let dictoryPath = downloadDirectory
        
        let fileName = url.lastPathComponent
        
        let totalPath = "\(dictoryPath)/\(fileName)"
        
        return totalPath
    }
    
    /// 移除沙盒文件
    ///
    /// - Parameter url: 下载链接
    func removeFile(_ url: URL) {
        
        let totalPath = getFilePath(url)
        
        do {
            try _fileManager.removeItem(atPath: totalPath)
            
        } catch {
            print(error)
        }
        
    }
    
    /// 删除所有已经下载的文件
    func removeFileAll() {
        
        do {
            let fileNames = try _fileManager.contentsOfDirectory(atPath: downloadDirectory)
            
            for fileName in fileNames {
                try _fileManager.removeItem(atPath: fileName)
            }
        } catch {
            print(error)
        }
        
    }
    
    func initDownloadModel(_ task: URLSessionDataTask, _ url: URL?, _ destPath: String, _ stateBlock: XFDownloadStateBlock?, _ progressBlock: XFDownloadProgressBlock?, _ completionBlock: XFDownloadCompletionBlock?) -> XFDownloadModel {
        
        let downloadModel = XFDownloadModel()
        downloadModel.dataTask = task
        downloadModel.url = url
        downloadModel.destPath = destPath
        downloadModel.stateBlock = stateBlock
        downloadModel.progressBlock = progressBlock
        downloadModel.completionBlock = completionBlock
        
        return downloadModel
    }
    
    /// 创建文件夹
    ///
    /// - Parameter path: 路径
    func createDirectory(_ path: String) {
        
        var isDirectory: ObjCBool = ObjCBool(false)
        
        let isExists = _fileManager.fileExists(atPath: path, isDirectory: &isDirectory)
        
        if !isExists || !isDirectory.boolValue {
            do {
                try _fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
                
            } catch {
                print(error)
            }
        }
        
        print(path)
    }
}

